package sk.uniza.fri.cerviix.server;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Random;

import sk.uniza.fri.cerviix.server.game.Player;
import sk.uniza.fri.cerviix.server.game.QuadTree;
import sk.uniza.fri.cerviix.server.game.Vector;
import sk.uniza.fri.cerviix.server.game.Worm;
import sk.uniza.fri.cerviix.server.game.WormPart;
import sk.uniza.fri.cerviix.server.network.Client;
import sk.uniza.fri.cerviix.server.network.ClientMessage;
import sk.uniza.fri.cerviix.server.network.NetworkManager;
import sk.uniza.fri.cerviix.server.network.messages.server.GameEndMessage;
import sk.uniza.fri.cerviix.server.network.messages.server.GameStartMessage;
import sk.uniza.fri.cerviix.server.network.messages.server.PlayerAddMessage;
import sk.uniza.fri.cerviix.server.network.messages.server.PlayerRemoveMessage;
import sk.uniza.fri.cerviix.server.network.messages.server.UpdateMessage;

public class Game
{
	public static final float SIZE_X = 50.0f, SIZE_Y = 50.0f;
	public static final long GAME_PAUSE = 3000;
	public static final int MAX_SPEED = 100;
	
	private Random random = new Random();
	
	private NetworkManager networkManager;
	
	private Map<Integer, Player> players = new HashMap<Integer, Player>();
	
	private List<Worm> worms = new ArrayList<Worm>();
	private int wormsAlive;
	
	private QuadTree board;
	
	private boolean go;
	
	private boolean playing = false;
	private int speed = MAX_SPEED;
	
	private long plannedGameStart = -1;
	
	public Game(NetworkManager networkManager)
	{
		this.networkManager = networkManager;
	}
	
	public void run()
	{
		Server.log.info("Main loop started.");
		
		go = true;
		
		while (go)
		{
			checkDisconnectedPlayers();
			processMessages();
			
			if (players.size() >= 2 && !playing)
			{
				if (plannedGameStart == -1)
				{
					Server.log.info("More than 1 player connected, preparing start...");
					
					plannedGameStart = System.currentTimeMillis() + GAME_PAUSE;
				}
				else
				{
					if (plannedGameStart <= System.currentTimeMillis())
					{
						Server.log.info("Game started!");
						
						playing = true;
						
						prepareMatch();
						
						for (Player player : players.values())
						{
							player.getClient().sendMessage(new GameStartMessage());
						}
					}
				}
			}
			else if (players.size() < 2 && !playing && plannedGameStart != -1)
			{
				Server.log.warning("Aborting start, not enough players (leavers)...");
				
				plannedGameStart = -1;
			}
			
			if (playing)
			{
				calculate();
				
				updatePlayers();
				
				checkMatch();
			}
			
			try
			{
				Thread.sleep(speed);
			}
			catch (InterruptedException e)
			{
			}
		}
		
		Server.log.info("Game stopped");
	}
	
	private void checkDisconnectedPlayers()
	{
		List<Player> playersToRemove = new LinkedList<Player>();
		
		for (Player player : players.values())
		{
			if (player.getClient() == null)
			{
				playersToRemove.add(player);
			}
		}
		
		for (Player player : playersToRemove)
		{
			removePlayer(player);
		}
	}
	
	private void processMessages()
	{
		while (networkManager.hasMessage())
		{
			networkManager.getMessage().run(this);
		}
	}
	
	private void prepareMatch()
	{
		board = new QuadTree(-SIZE_X, SIZE_X, -SIZE_Y, SIZE_Y);
		worms.clear();
		
		for (Player player : players.values())
		{
			Worm worm = new Worm(generateStartPosition(), generateStartAngle());
			
			player.addWorm(worm);
			
			worms.add(worm);
		}
		System.out.println("worms size: " + worms.size());
		wormsAlive = worms.size();
	}
	
	private void stopMatch()
	{
		playing = false;
		plannedGameStart = -1;
		speed = MAX_SPEED;
	}
	
	private void calculate()
	{				
		for (Worm worm : worms)
		{
			if (!worm.isAlive())
			{
				continue;
			}
			
			worm.update();
			
			float x, y;
			x = worm.getPosition().getX();
			y = worm.getPosition().getY();
			
			if (x < -50 || x > 50 || y < -50 || y > 50)
			{
				System.out.println("worm killed - went outside");
				killWorm(worm);
				continue;
			}
			
			List<WormPart> wormParts = board.getPartsInsideBox(x - 1, x + 1, y - 1, y + 1);
			
			for (WormPart part : wormParts)
			{
				if (part.worm == worm && part.index + 20 > worm.partsCount())
				{
					continue;
				}
				
				Vector position = worm.getPosition();
				Vector distance = new Vector(part.x, part.y);
				
				distance.sub(position);
				if (distance.getLength() < 1.0f)
				{
					System.out.println("worm killed - collision");
					killWorm(worm);
					break;
				}
			}
			
			if (worm.isAlive())
			{
				board.addWormPart(new WormPart(worm, (worm.partsCount() + 1), x, y));
				worm.partAdded();
			}
		}
	}
	
	private void killWorm(Worm worm)
	{
		if (!worm.isAlive())
		{
			throw new RuntimeException("Trying to kill killed worm!");
		}
		
		worm.kill();
		
		wormsAlive--;
		System.out.println("worms left " + wormsAlive);
		
		if (speed >= 10)
		{
			speed -= speed / worms.size();
		}
	}
	
	private void updatePlayers()
	{
		for (Player player : players.values())
		{
			updatePlayer(player);
		}
	}
	
	private void updatePlayer(Player player)
	{
		Client client = player.getClient();
		
		for (Player playerData : players.values())
		{
			if (playerData.getWorm() == null)
			{
				continue;
			}
			
			UpdateMessage update = new UpdateMessage();
			
			Worm worm = playerData.getWorm();
			Vector position = worm.getPosition();
			
			update.add(playerData.getId(), playerData.getScore().get(), worm.isAlive(), position.getX(), position.getY());
			
			client.sendMessage(update);
		}
	}
	
	private void checkMatch()
	{
		Player winner = null;
		boolean isDraw = false;
		
		if (wormsAlive == 1)
		{
			for (Player player : players.values())
			{
				if (!player.hasWorm())
				{
					continue;
				}
				
				if (player.getWorm().isAlive())
				{
					winner = player;
				}
			}
		}
		else if (wormsAlive < 1)
		{
			isDraw = true;
		}
		
		if (winner != null || isDraw)
		{
			stopMatch();

			int winnerId = (isDraw) ? 0 : winner.getId();
			
			for (Player player : players.values())
			{
				player.getClient().sendMessage(new GameEndMessage(winnerId));
				
				if (player.hasWorm())
				{
					if (winner != null && player == winner)
					{
						player.getScore().add();
					}
					else
					{
						player.getScore().sub();
					}
				}
			}
			
			updatePlayers();
		}
	}
	
	public void addPlayer(Player newPlayer)
	{			
		Client client = newPlayer.getClient();
		
		for (Player player : players.values())
		{
			player.getClient().sendMessage(new PlayerAddMessage(newPlayer.getId(), newPlayer.getName()));
			
			client.sendMessage(new PlayerAddMessage(player.getId(), player.getName()));
		}
		
		players.put(newPlayer.getId(), newPlayer);
		
		client.sendMessage(new PlayerAddMessage(newPlayer.getId(), newPlayer.getName()));
		
		Server.log.info(String.format("Added player: %s %d", newPlayer.getName(), newPlayer.getId()));
	}
	
	public void removePlayer(Player playerToRemove)
	{
		Worm worm = playerToRemove.getWorm();
		
		if (worm != null && worm.isAlive())
		{
			killWorm(worm);
		}
		
		players.remove(playerToRemove.getId());
		
		for (Player player : players.values())
		{
			player.getClient().sendMessage(new PlayerRemoveMessage(playerToRemove.getId()));
		}
		
		Server.log.info(String.format("Removed player: %s", playerToRemove.getName()));
	}

	public Vector generateStartPosition()
	{
		boolean isPositionOk = false;
		
		float x = 0.0f, y = 0.0f;
		
		while (!isPositionOk)
		{
			x = (float)random.nextInt((int)SIZE_X * 2) - SIZE_X;
			y = (float)random.nextInt((int)SIZE_Y * 2) - SIZE_Y;
			
			Vector distance = new Vector(x, y);
			
			boolean isTooNear = false;
			
			for (Worm worm : worms)
			{
				distance.sub(worm.getPosition());
				
				if (distance.getLength() < 10.0f)
				{
					isTooNear = true;
					break;
				}
			}
			
			if (isTooNear)
			{
				continue;
			}
			
			if (x < (-SIZE_X) + 10 || x > SIZE_X - 10 || y < (-SIZE_Y) + 10 || y > SIZE_Y - 10)
			{
				continue;
			}
			else
			{
				isPositionOk = true;
			}
		}
		
		return new Vector(x, y);
	}
	
	public float generateStartAngle()
	{
		return (float)random.nextInt(360);
	}
	
	public List<Worm> getWorms()
	{
		return worms;
	}
}
